Skip to content

fix(extensions): register commands on enable after an integration change - #5014

Open
yxshee wants to merge 3 commits into
github:mainfrom
yxshee:fix/extension-enable-after-integration-switch
Open

yxshee wants to merge 3 commits into
github:mainfrom
yxshee:fix/extension-enable-after-integration-switch

Conversation

@yxshee

@yxshee yxshee commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Description

An extension that is disabled while the project changes integration comes back "enabled" with no commands for the new agent. docs/reference/extensions.md describes disable as temporary ("Re-enable with enable"), but here re-enabling silently restores nothing:

$ specify init p --integration claude --ignore-agent-tools --non-interactive && cd p
$ specify extension add --dev ../jira          # one command: speckit.jira.sync-issues
$ specify extension disable jira
$ specify integration switch gemini
$ specify extension enable jira
✓ Extension 'Jira' enabled
# .specify/extensions/.registry -> jira: enabled=True, registered_commands={}
# ls .gemini/commands | grep jira -> nothing

integration install gemini followed by integration use gemini gives the same result. The only ways back are selecting the integration again or reinstalling the extension.

Root cause:

  • integration switch and integration use register only enabled extensions for the agent they activate (register_enabled_extensions_for_agent skips disabled entries).
  • switch also removes a disabled extension's commands from the previous agent.
  • extension enable (extensions/command_enable.py) then only flips the registry flag and re-enables hooks. Every integration except generic gets no registration at all.

The fix:

  • register_enabled_extensions_for_agent() gains a keyword-only extension_ids filter, so a single extension can be registered.
  • For non-generic agents, enable calls it for this one extension, but only when the registry has no commands recorded for the active agent.
  • A plain disable/enable cycle, where the commands are still registered, leaves the agent's files untouched.
  • Agents that take extension commands as skills (e.g. Copilot with --skills) only get missing skill files written.

With the change, the same steps end with:

$ specify extension enable jira
✓ Extension 'Jira' enabled
# registry -> jira: enabled=True, registered_commands={'gemini': ['speckit.jira.sync-issues']}
# .gemini/commands now contains the jira command

Overlap: your open PRs #4959 and #4883 also edit extensions/command_enable.py. Neither touches this case, but they will conflict textually with this change. If #4959 lands first, this logic belongs in ExtensionManager.enable(), and the PR can be rebased onto that.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (if applicable): the reproduction above, on main and on this branch

New tests in tests/specify_cli/extensions/test_command_enable.py, each parametrized over claude and copilot with --skills:

  • test_enable_registers_commands_for_integration_switched_while_disabled: fails on main (the extension's SKILL.md is missing after enable) and passes with the change
  • test_enable_leaves_commands_still_registered_for_active_integration: the negative case. A hand-edited skill survives a disable/enable cycle and the registry is unchanged. It passes before and after.

The file gives 5 passed with the change (2 failed on main).

  • Focused suites (tests/specify_cli/extensions, test_command_switch, test_command_use, test_extensions, test_extension_skills, test_extension_registration, test_integration_generic): 1095 passed.
  • Full suite (macOS, Python 3.13): 12334 passed, 376 skipped. 11 failures in the tests/test_*_python_parity.py files occur identically on unmodified main on this machine under full-suite load; those files pass when run on their own (257 passed, 112 skipped).
  • uvx ruff@0.15.0 check src tests: clean

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (fill in the disclosure below)

AI disclosure: Claude Code (model: Claude Opus 5.5, max reasoning effort, autonomous agent mode) opened this PR on behalf of @yxshee. A Claude Code subagent (same model, autonomous) found the bug and wrote the fix and tests. The lead agent independently re-ran the new tests against main and the branch, reproduced the behavior above with the real CLI, reviewed the diff, and wrote this description. No human reviewed the change line by line before it was opened.

🤖 Generated with Claude Code

`integration switch` and `integration use` register only enabled
extensions for the agent they activate, and `switch` also removes a
disabled extension's commands from the previous agent. For every
integration except `generic`, `extension enable` only flipped the
registry flag and re-enabled hooks, so an extension disabled across a
switch or use came back "enabled" with no commands for the active
agent: no skill or command file and nothing for that agent in
`registered_commands`, until the integration is selected again or the
extension is reinstalled.

When the registry has no commands for the active agent, `enable` now
registers the extension through register_enabled_extensions_for_agent,
which gains an `extension_ids` filter so only that extension is touched.
A plain disable/enable cycle, where the commands are still registered,
leaves the agent's files alone; agents that take extension commands as
skills (Copilot with --skills) only get missing skill files written.

Assisted-by: Claude Code (model: Claude Opus 5.5, autonomous)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yxshee
yxshee requested a review from mnriem as a code owner October 17, 2026 19:16
@mnriem mnriem added the triage-must-have Verdict: high-value, important work for Spec Kit — do first label Oct 18, 2026
@mnriem
mnriem requested a max review from Copilot October 19, 2026 13:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Enable can overwrite existing development skills and leave inconsistent enabled state when registration fails.

2 open issues
What changed in this PR

Updates extension enablement to restore commands after the project changes integrations.

Changes:

  • Adds extension-scoped registration.
  • Registers missing commands for the active integration on enable.
  • Adds restoration and file-preservation regression tests.

Validation used source inspection and isolated checks; project tests and baseline comparisons were not run.

AI disclosure: GitHub Copilot, model/settings unknown, autonomous; reviewed and suggested tests for the requester.

File Description
tests/​specify_cli/​extensions/​test_command_enable.py Adds integration-switch and file-preservation tests.
src/​specify_cli/​extensions/​command_enable.py Registers missing active-integration commands during enable.
src/​specify_cli/​extensions/​__init__.py Adds an extension-ID registration filter.

Review effort: Max

Comment thread src/specify_cli/extensions/command_enable.py Outdated
Comment thread src/specify_cli/extensions/command_enable.py Outdated
Decide whether enable must register from the files on disk for the
active agent's layout, not from registered_commands: Copilot --skills
leaves that map empty even when its skills exist, so a plain
disable/enable re-registered and replaced a --dev install's symlinked
skills with regular files. A stale entry could also hide skills that
are actually missing. ExtensionManager.missing_invocation_artifacts()
lists what is absent (a dev symlink counts as present), and enable only
registers when something is missing.

Registration swallows per-extension write failures, and setup errors can
escape it, either way leaving enabled=True with no invocation files and
a retry refused as "already enabled". Re-check the files afterwards and,
on a remaining gap or any exception, restore enabled=False and exit 1
before hooks are re-enabled, as the generic branch already does.

Assisted-by: Claude Code (model: Claude Opus 5.5, autonomous)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yxshee

yxshee commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Review round 1, posted on behalf of @yxshee.

Addressed both Copilot findings (commit 891bacc):

  • Missing-only, mode-aware registration: ExtensionManager.missing_invocation_artifacts() checks the active agent's layout on disk; enable registers only when files are missing, so existing files and dev symlinks are preserved.
  • Verified outcome: after registering, missing files or any exception restore enabled: false and exit 1, so the next enable can retry.
  • New tests: dev-symlink preservation (claude, copilot --skills) and rollback-then-retry for claude, copilot --skills and gemini with silent and raising failures. They fail on the previous head and pass now.
  • Extension and integration suites: 2129 passed. uvx ruff@0.15.0 check src tests: clean.

The overlap with #4959 noted in the description still applies.

AI disclosure: posted on behalf of @yxshee by Claude Code (model: Claude Opus 5.5, max reasoning effort, autonomous agent mode); the fix, tests and this reply are AI-generated.

🤖 Addressed by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Recovery can overwrite existing files, while artifact verification mishandles supported directories and partial registration failures.

4 open issues
2 resolved since last review

Review effort: Max

Comment thread src/specify_cli/extensions/__init__.py Outdated
Comment thread src/specify_cli/extensions/__init__.py Outdated
Comment thread src/specify_cli/extensions/__init__.py Outdated
Comment thread src/specify_cli/extensions/command_enable.py Outdated
@mnriem

mnriem commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

@mnriem mnriem added the author-awaiting Waiting on author response label Oct 19, 2026
…ndo failures

Round-2 review of the enable repair:

- Check every output registration writes for the active layout, under
  the directory registration itself resolves: home-relative (Hermes
  ~/.hermes/skills) and legacy (OpenCode/Kilo) directories, aliases, and
  Copilot's companion .prompt.md, not only the primary name.
- An unusable skills directory now raises, so enable restores the
  disabled state instead of reporting success without skills.
- Register only when none of the extension's outputs exist, the state an
  integration switch leaves behind. Registration rewrites every command,
  so repairing one missing output could discard edits or dev symlinks on
  the others; partially present outputs are left untouched.
- Because every output was absent beforehand, a failed attempt removes
  the outputs it wrote and restores the registry tracking, so a retry
  registers the full set instead of exiting as already complete.

Assisted-by: Claude Code (model: Claude Opus 5.5, autonomous)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yxshee

yxshee commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Review round 2, posted on behalf of @yxshee.

Addressed all four Copilot findings (commit 629224f):

  • Outputs are checked under the directory registration resolves (home-relative Hermes, legacy OpenCode/Kilo), including aliases and Copilot's companion .prompt.md.
  • An unusable skills directory raises, so enable stays disabled and exits 1.
  • Enable registers only when none of the extension's outputs exist, so surviving files and dev symlinks are never rewritten; partially present outputs are left untouched.
  • A failed registration removes the outputs it wrote and restores registry tracking, so a retry registers the full set.
  • Five new tests, each failing on the previous head. Extension and integration suites: 2134 passed. uvx ruff@0.15.0 check src tests: clean.

AI disclosure: posted on behalf of @yxshee by Claude Code (model: Claude Opus 5.5, max reasoning effort, autonomous agent mode); the fix, tests and this reply are AI-generated.

🤖 Addressed by Claude Code

@mnriem mnriem removed the author-awaiting Waiting on author response label Oct 19, 2026
@mnriem
mnriem requested a max review from Copilot October 19, 2026 15:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Registration failure handling still has verified correctness and data-loss risks.

2 open issues
1 previously missed issue

In code that hasn't changed since last review

Medium severity Skip registrar setup for hook-only extensions

src/​specify_cli/​extensions/​__init__.py:3955

Return for an empty manifest.commands before constructing CommandRegistrar. Its constructor loads installed integrations and raises if a package was modified or lacks a local trust decision (agents.py:53–65, integrations/installer.py:825–833). The new enable path therefore rejects hook-only extensions even though they have no invocation files to register; previously the event-refresh failure was only a warning. Match the early return in _register_commands_for_active_agent() and add a hook-only regression with registrar setup failure.

AI disclosure: GitHub Copilot (model/settings unknown, autonomous), for the requesting reviewer; generated this feedback from targeted source inspection.

4 resolved since last review

Review effort: Max

Comment on lines +33 to +36
manager.register_enabled_extensions_for_agent(agent, extension_ids={extension_id})
missing = sorted(name for name, path in outputs if not _present(path))
if missing:
raise ExtensionError("Missing invocation artifacts: " + ", ".join(missing))
Comment on lines +38 to +40
for _, path in outputs:
if path.is_symlink() or path.is_file():
path.unlink()
@mnriem

mnriem commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

@mnriem mnriem added the author-awaiting Waiting on author response label Oct 19, 2026
@yxshee

yxshee commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Review round 3, posted on behalf of @yxshee. No code changes in this round; here is why.

The two open Copilot findings share one root cause:

  • Registration failures masked when outputs exist (r4315795015): a write that creates or truncates a file before raising passes enable's on-disk check.
  • Rollback can't restore files retired from the previous layout (r4315795095): a commands→skills migration deletes the old agent/prompt files during registration, before enable can validate anything.

register_enabled_extensions_for_agent() deliberately isolates per-extension failures and swallows them, and it retires previous-layout artifacts in the same pass. From enable, the outcome can only be inferred from the filesystem afterwards. That inference can't detect truncated content, and it can't bring back files the registration pass already removed. A sound fix needs the registration layer itself to report a per-extension outcome, and to defer layout retirement until the replacement outputs are validated. That's a change to the shared registration contract, which #4959 is restructuring (it moves enable/disable into ExtensionManager.enable()).

So rather than add a third layer of checks around a helper that hides its errors, I'd suggest one of these:

Which would you prefer?

AI disclosure: written by Claude Code (model: Claude Opus 5.5, max reasoning effort, autonomous agent mode) on behalf of @yxshee; this comment is AI-generated.

🤖 Addressed by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author-awaiting Waiting on author response triage-must-have Verdict: high-value, important work for Spec Kit — do first

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants